Skip to content

Add OANDA RestV20 Historical Instrument Reader #257

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed

Conversation

davidandreoletti
Copy link

No description provided.

@davidandreoletti
Copy link
Author

@femtotrader I create a new Pull Request for several reasons:

I will have a look at implementing some of your suggestions below.

Thanks @davidandreoletti

a) But you could simply give this PR a title starting with WIP (work in progress) and still push there.
b) In order to provide a better user API experience granularity could be a Pandas offset shortcut string (like '5Min', '5H' ... in resample function ).
c) And it could be convert to a string compatible with Oanda API.
d) We could have a parameter named freq for DataReader function (it's role is quite similar to pandas.date_range parameter)

See #199

I'd convert freq (string) to Pandas DateOffset
http://stackoverflow.com/questions/27368265/convert-freq-string-to-dateoffset-in-pandas

and have a dict to convert Pandas DateOffset to Oanda granularity string.

Ask me if you don't understand what I mean (because I'm not english native speaker) so I may miss something.

"1T":"M1",
"1min":"M1",
"2T":"M2",
"2min":"M2",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you define SUPPORTED_OFFSET_ALIASES

You should have Pandas Offset as key not string shortcut as key.
Because it urge you to repeat several times some values.

In [1]: from pandas.tseries.frequencies import to_offset

In [2]: to_offset("1Min")
Out[2]: <Minute>

In [3]: offset = to_offset("1Min")

In [4]: offset
Out[4]: <Minute>

In [5]: type(offset)
Out[5]: pandas.tseries.offsets.Minute

Copy link
Author

@davidandreoletti davidandreoletti Nov 8, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@femtotrader OANDA REST v20 only accept predefined freqencies. Eg: 1T is supported since M1 is an accepted value for OANDA REST v20's endpoint however 7T -> M7 is not supported since M7 accepted by the endpoint.

Does it still make sense to have pandas.tseries.offsets.XXXX object instead of hardcoded Panda Offset aliases ?

Copy link
Contributor

@femtotrader femtotrader Nov 8, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will use this to_offset in a try/catch

so if freq can't be convert to Pandas Offset using to_offset it will be passed to Oanda directly.

On the other side I don't think it's a problem if user pass freq="7T" which will be convert to <7 * Minutes> but it won't be in SUPPORTED_OFFSET_ALIASES dict keys so we can decide here to raise an Exception to inform user that it's no an accepted timeframe.

Should we define keys for "1min", "1MIN", "1Min"...

In [9]: to_offset("1min")
Out[9]: <Minute>

In [10]: to_offset("1Min")
Out[10]: <Minute>

In [11]: to_offset("1MIN")
Out[11]: <Minute>

I don't think so!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so

Copy link
Author

@davidandreoletti davidandreoletti Nov 8, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@femtotrader Ok. So i am thinking of accepting a string or DateOffset for freq param

How can a DateOffset object be converted back to string ? Eg:

In[1]d = to_offset("1min") 
In[2] ds = to_offset_string(d)
Out[2] print(ds) # Displays "1min"

Copy link
Contributor

@femtotrader femtotrader Nov 8, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure a DateOffset can be convert back to a "normalized" offset string

In [20]: offset = to_offset("7T")

In [21]: offset.
                 offset.apply       offset.delta       offset.kwds        offset.nanos       offset.rollback
                 offset.apply_index offset.freqstr     offset.n           offset.normalize   offset.rollforward
                 offset.copy        offset.isAnchored  offset.name        offset.onOffset    offset.rule_code

but it could be an interesting feature to have in Pandas

In [18]: offset.rule_code
Out[18]: 'T'

In [20]: offset = to_offset("7T")

In [21]: offset.n
Out[21]: 7

In [22]: offset.rule_code
Out[22]: 'T'

In [23]: offset = to_offset("7Min")

In [24]: offset.n
Out[24]: 7

In [25]: offset.rule_code
Out[25]: 'T'

a quick and dirty approach could be

In [47]: def normalized_offset_string(offset):
    ...:      if offset.n == 1:
    ...:          return offset.rule_code
    ...:      else:
    ...:          return str(offset.n) + offset.rule_code

@femtotrader
Copy link
Contributor

femtotrader commented Nov 8, 2016

There is in fact an attribute to get normalized string from DateOffset it's named: freqstr

In [63]: from pandas.tseries.frequencies import to_offset

In [64]: to_offset("7Min").freqstr
Out[64]: '7T'

In [65]: to_offset("7MIN").freqstr
Out[65]: '7T'

In [66]: to_offset("7min").freqstr
Out[66]: '7T'

@femtotrader
Copy link
Contributor

I'm sorry I don't know "all" Pandas ;-)

@davidandreoletti
Copy link
Author

@femtotrader That's fine :) I only expect you to know the answers to the questions I ask :P

@davidandreoletti davidandreoletti changed the title [WIP] Add OANDA RestV20 Historical Instrument Reader Add OANDA RestV20 Historical Instrument Reader Nov 8, 2016
@davidandreoletti
Copy link
Author

@femtotrader My PR is done.

"5T":"M5",
"5min":"M5",
"15T":"M15",
"15min":"M15",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

freqstr DateOffset attribute should avoid to repeat these keys

In [67]: to_offset("15min").freqstr
Out[67]: '15T'

In [68]: to_offset("15T").freqstr
Out[68]: '15T'

In [69]: to_offset("15Min").freqstr
Out[69]: '15T'

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@femtotrader Fixed.

current_end_rfc3339 = current_end.strftime(rfc3339)

if isinstance(freq, DateOffset):
offsetString = self._normalized_offset_string(freq)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

offsetString = freq.freqstr

Sorry;-)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@femtotrader Fixed.

PS: No worries at all :)

@@ -56,7 +57,8 @@ def get_quote_google(*args, **kwargs):


def DataReader(name, data_source=None, start=None, end=None,
retry_count=3, pause=0.001, session=None, access_key=None):
retry_count=3, pause=0.001, session=None,
custom=None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

access_key is required for Enigma datareader.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@femtotrader Fixed.

oa = DataReader("EUR_USD", data_source="oanda_rest_historical_currency",
custom={ "accountType"="practice",
"accountVersion="0"
"apiToken":"Your private API token" })
Copy link
Contributor

@femtotrader femtotrader Nov 8, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oa = DataReader("EUR_USD", data_source="oanda_rest_historical_currency",
    custom = {
        "accountType": "practice",
        "accountVersion": "0",
        "apiToken": "Your private API token"
    })

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@femtotrader Fixed

@femtotrader
Copy link
Contributor

You need to run locally

flake8 --ignore E501 pandas_datareader

to fix some Flake8 errors

@davidandreoletti
Copy link
Author

@femtotrader PR done :)

"15T": "M15",
"30T": "M30",
"H": "H1",
"1H": "H1",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from pandas.tseries.frequencies import to_offset

to_offset("1H").freqstr
Out[20]: 'H'

to_offset("H").freqstr
Out[21]: 'H'

so we need just "H", not "1H"

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

"15S": "S15",
"30S": "S30",
"T": "M1",
"1T": "M1",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1T key should be removed (same as 1H)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

"6H": "H6",
"8H": "H8",
"12H": "H12",
"1D": "D",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be

"D": "D",

because

In: to_offset("1D").freqstr
Out: 'D'

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

"8H": "H8",
"12H": "H12",
"1D": "D",
"1W": "W",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"W": "W",

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

"12H": "H12",
"1D": "D",
"1W": "W",
"1M": "M"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"M": "M"

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

df.rename(inplace=True,
index=str,
columns={
"time": "date",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most DataReader index name is Date

"time": "Date",

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

"bid.c": "bid.close",
})

df = df.set_index('date')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

df = df.set_index('Date')

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A DataFrame with a MultiIndex as columns will be a great feature to have
http://pandas.pydata.org/pandas-docs/stable/advanced.html

Most other DataReader with candlestick data are using; Open, High, Low, Close column name (not open, high, low, close)

Here is an example to show how a DataFrame with MultiIndexed columns works

In: periods = 10
In: idx = pd.date_range("2010-01-01", freq="D", periods=10)
In: columns = [np.array(['Mid', 'Mid', 'Mid', 'Mid', 'Ask', 'Ask', 'Ask', 'Ask', 'Bid', 'Bid', 'Bid', 'Bid']),
              np.array(['Open', 'High', 'Low', 'Close', 'Open', 'High', 'Low', 'Close', 'Open', 'High', 'Low', 'Close'])]

In: df = pd.DataFrame(np.random.randn(periods, 12), columns=columns, index=idx)

In: df
Out[59]: 
                 Mid                                     Ask            \
                Open      High       Low     Close      Open      High   
2010-01-01 -0.357908  0.638564 -0.808755  0.029079  0.150620  0.282146   
2010-01-02  1.015372 -0.624254  0.353841  1.491935 -0.322171  0.353117   
2010-01-03  0.051334  0.516878 -1.769581  0.812352 -0.528826  0.677380   
2010-01-04 -1.681174  0.411583 -1.032651  1.924580  0.162397 -0.422782   
2010-01-05  0.512275 -0.112695 -0.705641  0.715283  0.446572 -1.428909   
2010-01-06 -1.197475 -0.025064 -0.997523  1.252269  0.217094 -1.086155   
2010-01-07 -0.935145 -0.446475 -1.220699 -1.135570  0.369933  1.695358   
2010-01-08 -0.259295  0.164924  0.371293 -0.136309  2.280143 -1.256588   
2010-01-09 -0.392548  1.762788 -1.656464  1.548067 -0.333061  0.226948   
2010-01-10 -0.431441  0.674132 -1.381039  0.892813  0.793012 -0.755067   

                                     Bid                                
                 Low     Close      Open      High       Low     Close  
2010-01-01 -1.313252 -1.179413  1.873345 -0.719676 -2.595192  1.182884  
2010-01-02 -0.670126  0.421320  0.318873 -0.756884  1.189023 -0.785939  
2010-01-03 -1.027978  0.609855  0.877085 -0.767293 -1.859567  0.662125  
2010-01-04  1.049496 -1.062435  0.577158 -0.068621 -0.080246 -0.786441  
2010-01-05 -3.193001 -0.540472  2.350964 -1.983940  2.278458 -0.783962  
2010-01-06 -0.101567 -0.145842 -1.600600  0.098544  0.133307  0.262338  
2010-01-07 -0.263570 -1.012718 -0.579255  1.360744  1.661800 -2.185659  
2010-01-08 -2.260959 -1.143676 -0.900669  0.721042 -0.421265 -0.716886  
2010-01-09 -1.229502 -0.442861 -0.767872  0.734038 -0.403405  0.046703  
2010-01-10  1.522349  0.463781  0.522694 -0.700728 -0.726378 -1.144720 

df["Ask"]
Out[60]: 
                Open      High       Low     Close
2010-01-01  0.150620  0.282146 -1.313252 -1.179413
2010-01-02 -0.322171  0.353117 -0.670126  0.421320
2010-01-03 -0.528826  0.677380 -1.027978  0.609855
2010-01-04  0.162397 -0.422782  1.049496 -1.062435
2010-01-05  0.446572 -1.428909 -3.193001 -0.540472
2010-01-06  0.217094 -1.086155 -0.101567 -0.145842
2010-01-07  0.369933  1.695358 -0.263570 -1.012718
2010-01-08  2.280143 -1.256588 -2.260959 -1.143676
2010-01-09 -0.333061  0.226948 -1.229502 -0.442861
2010-01-10  0.793012 -0.755067  1.522349  0.463781

df["Ask"]["Open"]
Out[61]: 
2010-01-01    0.150620
2010-01-02   -0.322171
2010-01-03   -0.528826
2010-01-04    0.162397
2010-01-05    0.446572
2010-01-06    0.217094
2010-01-07    0.369933
2010-01-08    2.280143
2010-01-09   -0.333061
2010-01-10    0.793012
Freq: D, Name: Open, dtype: float64

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Use Open instead open, etc ...

I will look at A DataFrame with a MultiIndex later this week.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Added MultiIndex support. Thanks for the suggestion @femtotrader !


def get_credential(self):
return {'accountType': "practice",
'apiToken': "8106b9109c7c6b17d45ff215f6da9d42-1446c148ee0f4ac9274eb6c2be0784b7"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can "hide" this in an environment variable if you want (like Enigma)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Token removed from tests. Now users can specify the token via an env variable

self.access_credential['apiToken'] = ""

def read(self):
dfs = {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using an OrderedDict will keep order of symbols in this Panel

from pandas.compat import OrderedDict

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

def test_oanda_historical_currencypair(self):
start = "2014-03-19T09:00:00Z"
end = "2014-03-21T9:00:00Z"
symbols = ["EUR_USD"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it works with simply

symbols = "EUR_USD"

?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: string is now also accepted to represent a single currency pair.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice.
Great job on this PR!

pn = OANDARestHistoricalInstrumentReader(
symbols=symbols,
start=start, end=end,
freq="S5",
Copy link
Contributor

@femtotrader femtotrader Nov 8, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

freq="5T",

which will be convert to Pandas DateOffset
which will be convert to "5T" (using freqstr)
which will be find in dictionnary and return "S5"

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

def test_oanda_historical_currencypair2(self):
start = "2014-03-19T09:00:00Z"
end = "2014-03-21T09:00:00Z"
symbols = ["EUR_USD"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A test with several symbols to ensure that we get a Panel will be a good idea.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@davidandreoletti davidandreoletti changed the title Add OANDA RestV20 Historical Instrument Reader [WIP] Add OANDA RestV20 Historical Instrument Reader Nov 10, 2016
Copy link
Contributor

@femtotrader femtotrader left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OANDA_API_TOKEN is now set on Travis.

I'm not sure but we probably need to export this environment variable in .travis.yml

  • export OANDA_API_TOKEN=$OANDA_API_TOKEN

8H -> 8 hour candlesticks, day alignment
12H -> 12 hour candlesticks, day alignment
1D -> 1 day candlesticks, day alignment
1W -> 1 week candlesticks, aligned to start of week
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1W -> W

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

6H -> 6 hour candlesticks, day alignment
8H -> 8 hour candlesticks, day alignment
12H -> 12 hour candlesticks, day alignment
1D -> 1 day candlesticks, day alignment
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1D -> D

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

12H -> 12 hour candlesticks, day alignment
1D -> 1 day candlesticks, day alignment
1W -> 1 week candlesticks, aligned to start of week
1M -> 1 month candlesticks, aligned to first day of the month
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1M -> M

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

def test_oanda_historical_currencypair(self):
start = "2014-03-19T09:00:00Z"
end = "2014-03-21T9:00:00Z"
symbols = ["EUR_USD"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice.
Great job on this PR!

@@ -0,0 +1,313 @@
import pandas as pd
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import os

is required

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@davidandreoletti
Copy link
Author

OANDA_API_TOKEN is now set on Travis.

I'm not sure but we probably need to export this environment variable in .travis.yml

export OANDA_API_TOKEN=$OANDA_API_TOKEN

The initial API TOKEN I committed was a mistake and it has now been revoked. So I will not provide an API TOKEN. Alternatively, someone (else) can provide an OANDA API TOKEN, encrypt it and then store it as encrypted env variable (supported by Travis).

@davidandreoletti davidandreoletti changed the title [WIP] Add OANDA RestV20 Historical Instrument Reader Add OANDA RestV20 Historical Instrument Reader Nov 13, 2016
@davidandreoletti
Copy link
Author

@femtotrader PR done.

@femtotrader
Copy link
Contributor

femtotrader commented Nov 14, 2016

I added my API key in Travis. But I think you need to add

export OANDA_API_TOKEN=$OANDA_API_TOKEN

in .travis.yml

@davidandreoletti
Copy link
Author

@femtotrader Is OANDA_API_TOKEN now unset in Travis (or revoked) ? CI fails with 401 errors when running the tests for this reader.

In [1]: import pandas_datareader.data as web
In [2]: start, end = "2016-01-01", "2016-02-01"
In [3]: symbols = ["EUR_USD"]
In [4]: credentials["accountType"]="practise"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

practice

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

In [1]: import pandas_datareader.data as web
In [2]: start, end = "2016-01-01", "2016-02-01"
In [3]: symbols = ["EUR_USD"]
In [4]: credentials["accountType"]="practise"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

empty credentials dict is required

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

In [5]: credentials["apiToken"]="Your OANDA API token"
In [6]: pn = web.DataReader(
symbols, data_source="oanda_historical_currency",
start=start, end=end,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might be able to pass the freq parameter

@ischurov
Copy link

ischurov commented Dec 11, 2016

Hi, could you please comment on the proposed solution to pandas-dev/pandas#14826 ? (See this comment and below.)

@davidandreoletti
Copy link
Author

@femtotrader PR ready.

@femtotrader
Copy link
Contributor

I'd be very pleased to merge it... but CI is still failing.

I don't understand why.

Could you try to add simply

    print(self.access_credential)

after

    if 'apiToken' not in self.access_credential:
        self.access_credential['apiToken'] = os.getenv('OANDA_API_TOKEN')
        if self.access_credential['apiToken'] is None:
            raise ValueError(
                """Please provide an OANDA API token or set the OANDA_API_TOKEN environment variable\n
                If you do not have an API key, you can get one here: http://developer.oanda.com/rest-live/authentication/""")

You should show my API key (so this approach is not very secure... but enough here because I don't mind if you display it in a Travis build.)

See https://github.com/femtotrader/test

@davidandreoletti
Copy link
Author

davidandreoletti commented Dec 13, 2016

@femtotrader The API token is missing. See output. Once added back, I will revert the last commit.

@femtotrader
Copy link
Contributor

femtotrader commented Dec 13, 2016

That's very odd because I set in Travis

OANDA_API_TOKEN=9b7baf...-...903cd6

Put this token in code because I'm fed up with Travis

@femtotrader
Copy link
Contributor

About request and multithreading you might have a look at
http://stackoverflow.com/questions/38280094/python-requests-with-multithreading

@davidandreoletti
Copy link
Author

davidandreoletti commented Dec 13, 2016

@femtotrader

Put this token in code because I'm fed up with Travis

I advise you not to. OANDA may revoke this token at best or close your account for some breach (after all those are considered personal info)

I personally set the OANDA_API_TOKEN env variable in my bash scripts so my environment is quite close to Travis I suppose.

Are you using encrypted environment value or not ?

It seems like encrypted env value are not provided to untrusted builds such as the ones initiated by pull requests.

About request and multithreading you might have a look at http://stackoverflow.com/questions/38280094/python-requests-with-multithreading

Python is not well suited for multithreaded code due to GIL. I have limited GIL knownlegde and initially thought this was a concern for CPU intensive apps. Turns out it also has implications with IO as per the doc provided.

This PR uses oanda-api-v20 to access OANDA's endpoints. So I lodged a feature request to that project.

@hootnot
Copy link

hootnot commented Dec 16, 2016

@davidandreoletti
Use requests_mock to fake the API requests. That way you can bypass the token sharing problem.
Take a look at the tests in the oandapyV20 wrapper repo: https://github.com/hootnot/oanda-api-v20

@davidandreoletti
Copy link
Author

davidandreoletti commented Dec 18, 2016

@hootnot

a)Do you know why the environment variable OANDA_API_TOKEN's value is not visible in unit test run on Travis while in my personal environment it is visible ? If you do, would you mind explaining it to me, please ? :)

@femtotrader

b) if @hootnot's solution cannot be used, I don't have access to Travis to investigate the environment variable issue, can you investigate it or grant me temporary access to Travis ?

c) I added another reader OANDARestAccountInstrumentReader. It is returning hard coded data and is missing in the documentation because I could not get it to fetch real data with my personal account. What do you want to do with it ("keep it undocumented in the documentation + warning in the source code", "something else") ?

d) I think we should move the oandrest.py classes (+ perhaps oanda.py) into a proper python module. What do you think ?

@femtotrader
Copy link
Contributor

Pinging @sinhrks @davidastephens @jreback
Could you help on this ?

@hootnot
Copy link

hootnot commented Dec 18, 2016

@davidandreoletti @femtotrader

regarding a):

This is not how it works. Travis can never know the value of the OANDA_API_TOKEN this way. If you use it from your shell this works. But the shell Travis uses has the value not set, so it it empty. Travis gets its environment variables from .travis.yml. Structure described in: https://docs.travis-ci.com/user/environment-variables/

If you want to use the/your token in the tests (and I would advise you not to) you need to set it up the way Travis points out in their docs, using encryption. The owner of the repo must do this since it involves a procedure with keys.
You end up with a section in .travis.yml looking like:

env:
  global:
  - secure: xxxxxxxxxxxxxxxxxxxxxxx

But when you do it this way you share a token as a contributor to the repo-owner. You do not want that for both parties:

  • as a contributor you can revoke the token and tests would break, so the owner relies on a contributor here
  • as owner you have access to a token which was offered on certain conditions by the issueing party to the contributor.

If you do want to use the token in the tests this way I suggest the best solution is that the repo-owner obtains a token and does the Travis setup with that token. This way the owner has control and responsibility for the token and contributors can still run the tests.

So if you want to use encrypted environment variables, it is up to to repo-owner to set this up.

I use this encryption setup in https://github.com/hootnot/oanda-api-v20/ to deploy to pypi automatically when tagging a release.

I still think that you should use mocking to setup your tests. With mocking you can bypass the issue of the token. In the next example you find a Panda class that represents the same kind of flow as you use in OANDARestHistoricalInstrumentReader :

import sys
import json
import unittest

import oandapyV20
import oandapyV20.endpoints.instruments as instruments
import requests_mock

data = range(10)      # just some testdata, 
environment = "practice"
access_token = None
api = None

class Panda(object):
    # some test class supposing to read data from OANDA using oandapyV20
    # ( and it will if you pass it a valid token )
    # the access_token is not relevant since the request is mocked
    def __init__(self, access_token="xxx"):  
        self.client = oandapyV20.API(access_token=access_token)

    def get_data(self, instrument, params):
        r = instruments.InstrumentsCandles(instrument=instrument, params=params)
        # this will call the mocked URI and return the data you provided setting up the mock request
        rv = self.client.request(r)
        return rv


class OA(unittest.TestCase):

    def setUp(self):
            global access_token
            global api

            # override the environments values with the ones relevant for the mock requests
            setattr(sys.modules["oandapyV20.oandapyV20"],
                    "TRADING_ENVIRONMENTS",
                    {"practice": {
                     "stream": "https://test.com",
                     "api": "https://test.com",
                     }})
            # setup the client the regular way
            api = oandapyV20.API(environment=environment,
                      access_token=access_token)
            api.api_url = "https://test.com"


    @requests_mock.Mocker()
    def test_candles(self, mock_req):
        params = { "granularity": "M5", "count": 5}
        r = instruments.InstrumentsCandles(instrument="EUR_USD", params=params)
        mock_req.register_uri('GET', "{}/{}".format(api.api_url, r),
                              text=json.dumps(data))
        # the statements before registered the uri, when this one gets called with this parameters, it returns
        # the associated data

        # try that with this test class
        panda = Panda()
        pandadata = panda.get_data("EUR_USD", params=params)
        self.assertEqual(pandadata, data)



if __name__ == "__main__":

     unittest.main()

create some testdata representing the data you get in real from a request. Store this in a file or files along with your tests. Write some method to read this data to setup the mock request and you can simulate the IO with OANDA's REST V20.

Regarding d)

I think it is wise to split some things up. The oandarest.py contains a lot of constants:

  • the offset-aliases
  • the self.OANDA_* and self.DATAFRAME_*

These could be separated and that makes them importable in other code also, which is currently not the case. It also reduces the size of oandarest.py about 10% making it more readable.

@gliptak
Copy link
Contributor

gliptak commented Jul 11, 2017

@davidandreoletti Support for Oanda was removed with #366 Consider closing this issue

@davidandreoletti davidandreoletti mentioned this pull request Jul 11, 2017
@femtotrader
Copy link
Contributor

@davidandreoletti you did a great work on this PR but
I'm closing because of OANDA restrictions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants